package net.java.dev.hickory.testing;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.processing.Processor;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;

/**
 *   Represents a single compilation. Maintains an in memory file system, and class loader
 * for loading the results of a compilation.
 * <p>
 * The output (filesystem) of one compilation can be used as input (filesystem) for
 *  a subsequent compilation by using
 * the constructor which takes that previous compilation as an argument.
 * <p>
 * The classpath for the compilation includes the classpath of the current runtime.
 * <p> 
 * Example 
 * <pre>{@literal
 * public void testSuccessfulCompile() throws Exception {
 *        Compilation compilation = new Compilation();
 *        compilation.addSource("com.example.Foo")
 *                .addLine("package com.example;")
 *                .addLine(" public class Foo {")
 *                .addLine("    public static int answer() { return 42; }")
 *                .addLine("}");
 *        compilation.doCompile(new PrintWriter(System.out));
 *        assertEquals(compilation.getDiagnostics(),0,compilation.getDiagnostics().size());
 *        Class<?> fooClass = compilation.getOutputClass("com.example.Foo");
 *        Method answerMethod = fooClass.getDeclaredMethod("answer");
 *        Object answer = answerMethod.invoke(null);
 *        assertEquals(Integer.valueOf(42),answer);
 *    }
 * }</pre>
 * This example uses reflection to test a method in a class generated by the compilation. 
 * See {@link RuntimeTest} for a similar example that dynamically builds, compiles and runs a
 * test method.
 * @author Bruce
 */
public class Compilation  {
    
    private JavaCompiler compiler;
    private DiagnosticCollector<JavaFileObject> diagnostics;
    private MemFileManager jfm;
    private List<JavaFileObject> jfos;
    private boolean done;
    private boolean success;
    private List<Processor> processors ;
    
/** Prepare a clean compilation with an empty Memory File System. */
    public Compilation() {
        this(null);
    }
    
    /** Prepare a compilation that reuses the memory based file system (generated source and classfiles)
     * from a previous compilation.
     */
    
    public Compilation(Compilation previous) {
        if(previous != null && !previous.done) {
            throw new IllegalStateException("Previous compilation has not yet been compiled.");
        }
        compiler = ToolProvider.getSystemJavaCompiler();
        if(previous == null) {
            StandardJavaFileManager sjfm = compiler.getStandardFileManager(diagnostics,null,null);
            jfm = new MemFileManager(sjfm);
        } else {
            jfm = previous.jfm;
        }
        diagnostics = new DiagnosticCollector<JavaFileObject>();
        jfos = new ArrayList<JavaFileObject>();
    }

    /**
     * Return a new MemSourceFileObject which can be populated with source code to be
     * compiled during this compilation. The return result's addLine() method should be used 
     * to populate the source file with source code.
     * 
     * @param fqn The fully Qualified Name of the top level class or interface which
     * the source code will define.
     * @return a source file ready to have source content added to it.
     * @throws IllegalStateException if {@code doCompile()} has already been called.
     */
    public MemSourceFileObject addSource(String fqn) {
        if(done) throw new IllegalStateException("cannot call after doCompilation()");
        MemSourceFileObject result = new MemSourceFileObject(fqn);
        jfos.add(result);
        return result;
    }

    /** Specify an annotation processor to use during the compilation. Overrides the default
     * discovery mechanism. This method may be called multiple times in order to run multiple processors
     * during the compilation.
     * <p>If this method is not called, a compilation will use processors specified as command line arguments
     * or those found by the default discovery mechanism in the existing runtime's classpath and
     * in the memory file system as a result of a previous compilation. 
     * @param processor A processor to explicitly run during the compilation.
     * @throws IllegalStateException if {@code doCompile()} has already been called.
     */
    public void useProcessor(Processor processor) {
        if(done) throw new IllegalStateException("cannot call after doCompilation()");
        if(processors == null) processors = new ArrayList<Processor>();
        processors.add(processor);
    }
    
    /**
     * Perform the compilation of the source files created by addSource(). Running
     * any processors specified with useProcessor().
     * 
     * @param out A writer to use for non Diagnostics output from the compilation (such as System.out output
     * from the processors, and compiler or processor failure messages).
     * @param options Any other options for the compilation. see the output from javac command.
     * @throws IllegalStateException if {@code doCompile()} has already been called.
     *@return true if the compilation was successful (the value returned from CompilationTask.call())
     */
    public boolean doCompile(Writer out, String... options) {
        if(done) throw new IllegalStateException("cannot call after doCompilation()");
        List<String> optionList = new ArrayList<String>();
        optionList.addAll(Arrays.asList("-classpath",System.getProperty("java.class.path")));
        optionList.addAll(Arrays.asList(options));
        JavaCompiler.CompilationTask task = compiler.getTask(out,jfm,diagnostics,optionList,null,jfos);
        if(processors != null) task.setProcessors(processors);
        success = task.call();
        done = true;
        return success;
    }
    
    /** Return the list of diagnostics generated during the compilation. 
      * @throws IllegalStateException if {@code doCompile()} has not yet been called.
      */
    public List<Diagnostic<? extends JavaFileObject>> getDiagnostics() {
        if(!done) throw new IllegalStateException("cannot call before doCompilation()");
        return diagnostics.getDiagnostics();
    }
    
    /** Get a class resulting from the compilation (or a previous compilation used to 
     * initialize this one).
     *equivalent to {@code getOutputClassLoader().loadClass(fqn)}
     *@param fqn The fully qualified name of the class
     *@throws ClassNotFoundException if the class cannot be found
     *@throws IllegalStateException if {@code doCompile()} has not yet been called.
     */
    public Class<?> getOutputClass(String fqn) throws ClassNotFoundException {
        if(!done) throw new IllegalStateException("cannot call before doCompilation()");
        return jfm.getClassLoader(StandardLocation.CLASS_OUTPUT).loadClass(fqn);
    }
    
    /** Return the content of a source file generated by a processor.
     * @param fqn The fully qualified name of the class
     * @throws IllegalStateException if {@code doCompile()} has not yet been called.
     * returns null if the class was not generated.
     */
    public String getGeneratedSource(String fqn) {
        if(!done) throw new IllegalStateException("cannot call before doCompilation()");
        try {
            String name = fqn.replace(".","/") + ".java";
            JavaFileObject jfo = jfm.getJavaFileForOutput(
                    StandardLocation.SOURCE_OUTPUT,name,JavaFileObject.Kind.SOURCE,null);
            if(jfo == null) return null;
            
            return jfo.getCharContent(true).toString();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Access the memory file system.
     * If {@link #doCompile(java.io.Writer, java.lang.String[]) } has not yet been called a FileObject will be returned whether
     * or not it already exists. This is so that the file system can be populated with files prior to compiling (but use {@link #addSource(java.lang.String) }
     * for populating source files).
     * If {@link #doCompile(java.io.Writer, java.lang.String[]) } has already been called on this, and the file does not
     * exist in the ram file system, then null will be returned.
     * @see JavaFileManager#getFileForInput(javax.tools.JavaFileManager.Location, java.lang.String, java.lang.String)
     * @param loc The location to access.
     * @param packageName a package name.
     * @param relativeName a relative name.
     * @return the FileObject or null for a non existent file after compiling.
     */

    public FileObject getFile(JavaFileManager.Location loc, String packageName, String relativeName)  {
        if((!done) || jfm.fileExists(loc, packageName, relativeName)) {
            try {
                return jfm.getFileForOutput(loc, packageName, relativeName, null);
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        } else {
            // compilation done and file doesn't exist in ram file system
            return null;
        }
    }

    
    /** Return a runtime test that can be 
     * executed against the compiled code. The RuntimeTest may be executed multiple times
     * and passed different arguments if the code was defined to take arguments.
     * @param code The source code of the statements to be executed.
     * @throws IllegalStateException if {@code doCompile()} has not yet been called.
     * @throws IllegalArgumentException if the code does not compile.
     */ 
    public RuntimeTest createRuntimeTest(TestCode code) {
        if(!done) throw new IllegalStateException("cannot call before doCompilation()");
        return new RuntimeTest(code,jfm);
    }

    /** Return a classloader from which the compiled classes may be loaded. */
    public ClassLoader getOutputClassLoader() {
        return  jfm.getClassLoader(StandardLocation.CLASS_OUTPUT);
    }
}
